Las variables y las constantes son los objetos de datos básicos que se manipulan en un programa. Las declaraciones muestran las variables que se van a utilizar y establecen el tipo que tiene y algunas veces cuáles son sus valores iniciales. Los operadores especifican lo que se hará con las variables. Las expresiones combinan variables y constantes para producir nuevos valores. El tipo de un objeto determina el conjunto de valores que puede tener y qué operaciones se pueden realizar sobre él. Estos son los temas de este capítulo.
El estándar ANSI ha hecho muchos pequeños cambios
y agregados a los tipos ásicos y a las expresiones. Ahora
hay formas signed
y unsigned
de todos los
tipos enteras, y notaciones para constantes sin signo y constantes
de carácter hexadecimales. Las operaciones de punto flotante
pueden hacerse en precisión sencilla; también hay un
tipo long double
para precisiñon extendida. Las
constantes de cadena pueden concatenarse al tiempo de
compilación. Las enumeraciones son ya parte del lenguajem
formalizando una característica pendiente por mucho tiempo.
Los objetos pueden ser declarados const
, lo que impide
que cambien. Las reglas para conversión automatica entre
tipos aritméticos se aumentaron para manejar el ahora
más rico conjunto de tipos.
Aunque no lo mencionamos en el capítulo 1, existen
algunas restricciones en los nombres de las variables y de las
constantes simbólicas. Los nombres se componen de letras y
dígitos; el primer carácter debe ser una letra. El
carácter de subrayado "_" cuenta como una letra; algunas
veces es útil para mejorar la legibilidad de nombres largos
de variables. Sin embargo, no se debe comenzar los nombres de
variables con este carácter, puesto que las rutinas de
biblioteca con frecuencia usan tales nombres. Las letras
mayúsculas y minúsculas son distintas, de tal manera
que x
y X
son dos nombres diferentes. La
práctica tradicional de C es usar letras minúsculas
para nombres de variables, y todo en mayúsculas para
constantes simbólicas.
Al menos los primeros 31 caracteres de un nombre son
significativos. Para nombres de funciones y variables externas el
número puede ser menor que 31, puesto que los nombres
externas los pueden usar los ensambladores y los cargadores, sobre
los que el lenguaje no tiene control. Para nombres externos, el
estándar garantiza distinguir sólo para 6
carácteres y sin diferenciar mayúsculas de
minúsculas. Las palabras claves como if, else, int,
float,
etc., están reservadas: no se pueden utilizar
como nombres de variables. Todas ellas deben escribirse con
minúsculas.
Es conveniente elegir nombres que estén relacionados con el propósito de la variable, que no sea probable confundirlos tipográficamente. Nosotros tendemos a utilizar nombres cortos para variables locales, especialmente índices de iteraciones, y nombres más largos para variables externas.
Hay unos cuantos tipos de datos básicos en C:
char un solo byte, capaz de contener un carácter del conjunto de caracteres local. int un entero, normalmente del tamaño natural de los enteros en la máquina en la que se ejecuta float punto flotante de precisión normal. double punto flotante de doble precisión
Además, existen algunos calificadores que se aplican a
estos tipos básicos. short
y long
se aplican a enteros:
short int sh; long int counter;
La palabra int
puede omitirse de tales
declaraciones, lo que típicamente se hace.
La intención es que short
y
long
puedan proporcionar diferentes longitudes de
enteros donde sea práctico; int
será
normalmente el tamaño noatural para una máquina en
particular. A menudo short
es de 16 bits y
long
de 32; int
es de 16 o de 32 bits.
Cada compilador puede seleccionar libramente los tamaños
apropiados para su propio hardware, sujeto sólo a la
restricción de que los shorts
y
ints
son por lo menos 16 bits, los longs
son por lo menos de 32 bits y el short
no es mayor que
int
, el cual a su vez no es mayor que
long
.
El calificador signed
o unsigned
puede
aplicarse a char
o a cualquier entero. Los
números unsigned
son siempre positivos o cero y
obedecen las leyes de la aritmética módulo
2n, donde n es el número de bits en
el tipo. Así, por ejemplo, si los char
son de 8
bits, las variables unsigned char
tienen valores entre
0 y 255, en tanto que las variables signed char
tienen
valores entre -128 y 127 (en una máquina de complemento de
dos). El heco de que los chars
ordinarios sean con
signo o sin él depende de la máquina, pero los
caracteres que se pueden imprimir son siempre positivos.
El tipo long double
especifica punto flotante de
precisión extendida. Igual que con los enteros, los
tamaños de objetos de punto flotante se definen en la
implantación; float, double
y long
double
pueden representar uno, dos, o tres tamaños
distintos.
Los archivos de encabezado headers estándar
<limits.h>
y <float.h>
contienen constantes simbólicas para todos esos
tamaños, junto con otras propiedades de la máquina y
del compilador, los cuales se discuten en el apéndice B.
Ejercicio 2-1. Escriba un programa para
determinar los rangos de variables char, short, int
y
long
, tanto signed
como
unsigned
, imprimiendo los valores apropiados de los
headers estándar y por cálculo directo. Es
más difícil si los calcula: determine los rangos de
los varios tipos de punto flotante.
Una constante entera como 1234
es un
int
. Una constante long
se escribe con un
l
(ele) o L
terminal, como en
123456789L
; un entero demasiado grande para caber
dentro de un int
también será tomado
como long
. Las contantes sin signo se escriben con una
u
o U
terminal y el sufijo
ul
o UL
indica unsigned
long
.
Las constantes de punto flotante contienen un punto decimal
(123.4) o un exponente (1e-2
) o ambos; su tipo es
double
, a menos que tengan sufijo. Los sufijos
f
o F
indican una constante
float
; l
o L
indican un
long double
.
El valor de un entero puede especificarse en forma octal o
hexadecimal en lucar de decimal. Un 0
(cero) al
principio en una constante entera significa ocatl; 0x
o 0X
al principio significa hexadecimal. por ejemplo,
el decimal 31 puede escribirse como 037
en octal y
0x1f
ó 0X1F
en hexadecimal. Las
constantes octales y hexadecimales también pueden ser
seguidas por L
para convertirlas en long
y U
para hacerlas unsigned
:
0XFUL
es una constante unsigned long
con
valor de 15 en decimal.
Una constante de carácter es un entero, escrito
como un carácter dentro de apóstrofos, tal como
'x'
. El valor de una constante de carácter es
el valor numérico del carácter en el conjunto de
caracteres de la máquina. Por ejemplo, en el conjunto de
caracteres ASCII el carácter constante '0'
tiene el valor de 48, el cual no está relacionado con el
valor numérico 0
. Si escribimos
'0'
en vez de un valor numérico como 48 que
depende del conjunto de caracteres, el programa es independiente
del valor particular y más fácil de leer. Las
constantes de carácter participan en operaciones
numéricos tal como cualesquier otros enteros, aunque se
utilizan más comúnmente en comparaciones con otros
caracteres.
Ciertos caracteres pueden ser representados en constante de
carácter y de cadena, por medio de secuencias de escape como
\n
(nueva línea); esas secuencias se ven como
dos caracteres, pero representan sólo uno. Además, un
patrón de bits arbitrario de tamaño de un byte puede
ser especificado por
'\ooo'
en donde ooo son de uno a tres dígitos octales (0...7) o por
'\xhh
en donde hh sone uno o más dígitos hexadecimales (0...9, a...f, A...F). Así podríamos escribir
#define VTAB '\013' /* tab vertical ASCII */ #define BELL '\007' /* carácter campana ASCII */
o, en hexadecimal
#define VTAB '\xb' /* tab vertical ASCII */ #define BELL '\x7' /* carácter campana ASCII */
El conjunto completo de secuencias de escape es
\a carácter de alarma (campana) \b retroceso \f avance de hoja \n nueva línea \f regreso de carro \t tabulador horizontal \v tabulador vertical \\ diagonal invertida \? interrogación \' apóstrofo \" comillas \ooo número octal \xhh número héxadecimal
La constante de carácter '\0'
represente el
carácter con valor cero, el carácter nulo.
'\n'
a menudo se escribe en vez de 0
para
enfatizar la naturaleza de carácter de algunas expresiones,
pero el valor es precisamente 0
.
Una expresión constante es una expresión que sólo in miscuye constantes. Tales expresiones pueden ser evaluadas durante la compilación en vez de que se haga en tiempo de ejecución, y por tanto pueden ser utilizadas en cualquier lugar en que pueda encontrarse una constante, como en
#define MAXLINE 1000 char line[MAXLINE + 1];
o
#define LEAP 1 /* en años bisiestos */ int days [31 + 28 + LEAP + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31];
Una constante de cadena o cadena literal, es una secuencia de cero o más caracteres encerrados entre comillas, como en
"Soy una cadena"
o
"" /* la cadena vacía */
Las comillas no son parte de la cadena, sólo sirven para delimitarla. Las mismas secuencias de escape utilizadas en constantes de carácter se aplican en cadenas; \" representa el carácter comillas. Las constantes de cadena pueden ser concatenadas en tiempo de compilación:
"hola," "mundo"
es equivalente a
"hola, mundo"
Esto es útil para separar cadenas largas entre varias líneas fuente.
Técnicamente, una constante de cadena es un arreglo de
caracteres. La representación interna de una cadena tiene un
carácter nulo '\0'
al final, de modo que el
almacenamiento físco requerido es uno más del
número de caracteres escritos entre las comillas. Esta
representación significa que no hay límite en cuanto
a qué tan larga puede ser una cadena, pero los programas
deben leer completamente una cadena para determinar su longitud. La
función strlen(c)
de la biblioteca
estándar regresa la longitud de su argumento s
de tipo cadena de caracteres, excluyendo el '\0'
terminal. Aquí está nuestra versión:
int strlen(char s[]) { int i; i = 0; while (s[i] != '\0') ++i; return i; }
strlen
y otras funciones para cadenas están
declaradas en el header estándar
<string.h>
.
Se debe ser cuidadoso al distinguir entre una constante de
carácter y una cadena que contiene un sólo
carácter: 'x'
no es lo mismo que
"x"
. El primero es un entero, utilizado para producir
el valor numérico de la letra x en el conjunto de
caracteres de la máquina. El último es un arreglo de
caracteres que contiene un carácter (la letra x) y un
'\0'
.
Existe otra clase de constante, la constante de enumeración. Una enumeración es una lista de valores enteros constantes, como en
enum boolean {NO, YES};
El primer nombre en un enum
tiene valor
0
, el siguiente 1
, y así
sucesivamente, a menos que sean especificados valores
explícitos. Si no todos los valores son especificados
continúan la progresión a partir del último
valor que sí lo fue, como en el segundo de esos
ejemplos:
enum escapes { BELL = '\a', RETROCESO = '\b', TAB = '\t', NVALIN = '\n', VTAB = '\v', RETURN = '\r'}; enum months { ENE = 1, FEB, MAR, ABR, MAY, JUN, JUL, AGO, SEP, OCT, NOV, DIC; /* FEB es 2, MAR es 3, etc. */
Los nombres que están en enumeraciones diferentes deben ser distintos. Los valores no necesitan ser distintos dentro de la misma enumeración.
Las enumeraciones proporcionan una manera conveniente de asociar
valores constantes con nombres, una alternativa a
#define
con la ventaja de que los valores pueden ser
generados para uno. Aunque las variables de tipos enum
pueden ser declaradas, los compiladores no necesitan revisar lo que
se va a almacenar en tal variable es un valor válido para la
enumeración. No obstante, las variables de
enumeración ofrecen la oportunidad de revisarlas y tal cosa
es a menudo mejor que #define
. Además, un
depurador puede ser capaz de imprimir los valores de variables de
enumeración en su forma simbólica.
Todas las variables deben ser declaradas antes de su uso, aunque ciertas declaraciones pueden ser hechas en forma implícita por el contexto. Una declaración especifica un tipo, y contiene una lista de una o más variables de ese tipo, como en
int lower, upper, step; char c, line[1000];
Las variables pueden ser distribuidas entre las declaraciones en cualquier forma; la lista de arriba podría igualmente ser escrita como
int lower; int upper; int step; char c; char line[1000];
Esta última forma ocupa más espacio, pero es conveniente para agregar un comentario a cada declaración o para modificaciones subsecuentes.
Una variable también puede ser inicializada en su declaración. Si el nombre es seguido por un signo de igual y una expresión, la expresión sirve como un inicializador, como en
char esc = '\\'; int i = 0; int limit = MAXLINE + 1; float eps = 1.0e-5;
Si la variable en cuestión no es automática, la inicialización es efectuada sólo una vez, conceptualmente antes de que el programa inicie su ejecución, y el inicializador debe ser una expresión constante. Una variable explicitamente inicializada es inicializada cada vez que se entra a la función o bloque en que se encuentra; el inicializador puede ser cualquier expresión. Las variables automáticas para las que no hay un inicializador explícito tienen valores indefinidos (esto es, basura).
El calificador const
puede aplicarse a la
declaración de cualquier variable para especificar que su
valor no será cambiado. Para un arreglo, el calificador
const
indica que los elementos no serán
alterados.
const double e = 2.71828182845905; const char msg[] = "precaución: ";
La declaración const
también se puede
utilizar con argumentos de tipo arreglo, para indicar que la
función no cambia ese arreglo:
int strlen(const char[]);
Si se efectúa un intento de cambiar un
const
, el resultado está definido po la
implantación.
Los operadores arítmeticos binarios son +, -, *,
/,
y el operadoe módulo %
. La
división entera trunca cualquier parte fraccionaria. La
expresión
x % y
produce el residuo cuando x
es dividido entre
y
, por lo que es cero cuando y
divide a
x
exactamente. Por ejemplo, un año bisiesto si
es divisible entre 4 pero no entre 100, excepto aquellos
años que son divisibles entre 400, que sí son
bisiestos. Por lo tanto
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) printf("%d es un año bisiesto\n", year); else printf("%d no es un año bisiesto\n", year);
El operador %
no peued aplicarse a operandos
float
o double
. La dirección de
truncamiento para /
y el signo del resultado de
%
son dependientes de la máquina para operandos
negativos, asó como la acción que se toma en caso de
sobreflujo o subflujo.
Los operadores binarios +
y -
tienen
la misma precedencia, la cual es menor que la precedencia de
*, /,
y %
, que a su vez esmenor que
+
y -
unarios. Los operadores
aritméticos se asocian de izquierda a derecha.
La tabla 2-1 que se encuentra al final de este capítulo, resume la presedencia y asociatividad para todos los operadores.
Los operadores de relación son
> >= < <=
Todos ellos tienen la misma precedencia. Precisamente bajo ellos en precedencia están los operadores de igualdad:
== !=
Los operadores de relación tienen precedencia inferior
que los operadores aritméticos, así que una
expresión como i < lim-1
se toma como
i < (lim-1)
, como se esperaría.
Más interesantes son los operadores lógicos
&& y ||. Las expresiones conectadas por && o || son
evaluadas de izquierda a derecha, y la evaluación de detiene
tan pronto como se conoce el resultado verdadero o falso. La
mayoría de los programas en C descansan sobre esas
propiedades. Por ejemplo, aquí está un ciclo de la
función de entrada getline
que escribimos en el
capítulo 1:
for (i=0; i<lim-1 && (c=getchar()) != '\n' && c!= EOF; ++1) s[i] = c;
Antes de leer un nuevo carácter es cecesario verificar
que hay espacio para almacenarlo en el arreglo s
,
así que la prueba i < lim-1
debe
hacerse primero. Además, se esta prueba falla, no debemos
seguir y leer otro carácter.
De manera semejante, sería desafortunado si
c
fuese probado contra EOF
antes de que
se llama a getchar
; por lo tanto, la llamada y la
asignación deben ocurrir antes de que se pruebe el
carácter c
.
La precedencia de && es más alta que la de ||, y ambas son menores que los operadores de relación y de asignación, así que expresiones como
i<lim-1 && (c = getchar()) != '\n' && c != EOFno requieren de paréntesis adicionales. Pero puesto que la precedencia de != es superior que la asignación, los paréntesis se necesitan en
(c = getchar()) != '\n'
para obtener el resultado deseado de asignación a
c
y después comparación con
'\n'
.
Por definición, el valor numérico de una expresión de relación o lógica es 1 si la relación es verdadera, y 0 si la relación es falsa.
El operador unario de negación !
convierte a
un operador que no es cero en 0, y a un operador cero en 1. Un uso
común de !
es en construcciones como
if (!válido)
en lugar de
if (válido == 0)
Es difícil generalizar acerca de cuál es la mejor.
Construcciones como !válido
se leen en forma
agradable ("si no es valido"), pero otras más complicadas
pueden ser difíciles de entender.
Ejercicio 2-2. Escriba un ciclo equivalente a
la iteración for
anterior sin usar && o
||.
Cuando un operador tiene operandos de tipos diferentes,
éstos se convierten a un tipo común de acuerdo con un
reducida número de reglas. En general, las únicas
conversiones automáticas son aquellas que convierten un
operando "angosto" a uno "amplio" sin pérdida de
información, tal como convertir un entero a punto flotante
un una expresión como f + i
. Las expresiones
que podrián perder información, como asignar un tipo
mayor a uno más corto, o un tipo de punto flotante a un
entero, pueden producir una advertencia, pero no son ilegales.
Un char
sólo es un entero pequeño,
por lo que los char
se pueden utilizar libremente en
expresiones aritméticas. Esto permite una flexibilidad
considerable en ciertas clases de transformación de
caracteres. Una es ejemplificada con esta ingenua
implantación de la función atoi
, que
convierte una cadena de dógitos en su equivalente
numérico.
/* atoi: convierte s en entero */ int atoi(char s[]) { int i, n; n = 0; for (i=0; s[i] >= '0' && s[i] <= '9'; ++i) n = 10 * n + (s[i] - '0'); return n; }
tal como se discutió en el capítulo 1, la expresión
s[i] - '0'
da el valor numérico del carácter almacenado en
s[i]
, debido a que los valores de '0',
'1',
etc., forman una secuencia ascendente contigua.
Otro ejemplo de conversión de char
a
int
es la función lower
, que
convierte un carácter sencillo a minúscula para el
conjunto de caracteres ASCII. Si el carácter no es una
letra mayúscula, lower
lo regresa sin
cambio.
/* lower: convierte c a minúscula; solamente ASCII */ int lower(int c) { if (c >= 'A' && c <= 'Z') return c + 'a' - 'A'; else return c; }
Esto funciona para ASCII debido a que las correspondientes
letras mayúsculas y minúsculas están a una
distancia fija como valores numéricos y cada alfabeto es
contiguo --no hay sino letras entre A
y
Z
. Sin embargo, esta última observación
no es cierta para el conjunto de caracteres EBCDIC, así que
este código podría convertir algo más que
sólo letras en EBCDIC.
El header estándar <ctype.h>
,
que se describe en el apéndice B,
define una familia de funciones que proporcionan pruebas y
conversiones independientes de los juegos de caracteres. Por
ejemplo, la función tolower(c)
regresa el valor
de la letra minúscula de c
si c
es
una mayúscula, de modo que tolower
es un
reemplazo transportable para la función lower
mostrada antes. De modo semejante, la prueba
c >= '0' && c <= '9'
puede reemplazarse por
isdigit(c)
Nosotros utilizaremos las funciones de
<ctype.h>
en adelante.
Existe un sutil punto acerca de la conversión de
caracteres a enteros: El lenguaje no especifica si las variables de
tipo char
son valores con o sin signo. Cuando en
char
se convierte a int
, ¿puede
producir aluguna vez un entero negativo? La respuesta varía
de una máquina a otra, reflejando diferencias en la
arquitectura. En algunas máquinas un char
cuyo
bit más a la izquierda es 1 se conviertirá a un
entero negativo ("extensión de signo"). En otras, un
char
es promovido a un int
agregando
ceros del lado izquirdo, así que siempre es poitivo.
La definición de C garantiza que ningún
carácter que este en el conjunto estándar de
caracteres de impresión de la máquina será
negativo, de modo que esos caracteres siempre serán
cantidades positivas en las expresiones. Pero hay patrones
arbitrarios de bits almacenados en variables de tipo
carácter que pueden aparacer como negativos en algunas
máquinas, aunque sean positivos en otras. Por
transportabilidad, se debe especificar signed
o
unsigned
si se van a almacenar datos que no son
caracteres en variables tipo char
.
Las expresiones de relación como i > j
y
las expresiones lógicas conectadas por
&&
y ||
están definidas
para tener un valor de 1 siendo vardaderas, y 0 al ser falsos. De
este modo, la asignación
d = c >= '0' && <= '9'
hace 1 a d
si c
es un dígito, y
0 si no lo es. Sin embargo, las funciones como isdigit
pueden regresar cualquier valor diferente de cero como verdadero.
En la parte de validación de if, while, for,
etc., "verdadero" es sólo "diferente de cero", por lo que no
hace diferencia.
Las conversiones aritméticas implícitas trabajan
como se espera. En general, si un operador como + o * que toma dos
operadores (operario binario) tiene operandos de diferentes tipos,
el tipo "menor" es promovido al tipo "superior" antes de que
la operación proceda. El resultado es el del tipo mayor. La
sección 6 del apéndice A
establece las reglas de conversión en forma precisa. Si no
hay operandos unsigned
, sin embargo, el siguiente
conjunto informal de reglas bastará:
long double
,
conviértase el otro a long double
.double
,
conviértase el otro a double
.float
,
conviértase el otro a float
.char
y
short
a int
.long
,
conviértase el otro a long
.Nótase que los float
que están en una
expresión no se convierten automáticamente a
double
; esto es un cambio de la definición
original. En general, las funcioned matemáticas como las de
<math.h>
utilizarán doble
precisión. La razón principal para usar
float
es ahorrar espacio de almacenamiento en arreglos
grandes o, con menor frecuencia, ahorrar tiempo en máquinas
en donde la aritmética de doble precisión es
particularmente costosa.
Las reglas de conversión son más complicadas
cuando hay operandos unsigned
. El problema es que las
compariciones de valores con signo y sin signo son dependientes de
la máquina, debido a que dependen lo tamaños de los
varios tipos de enteros. Por ejemplo, supóngase que
int
es de 16 bits y long
de 32. Entonces
-1L < 1U
, debido a que 1U
, que es un
int
, es promovido a signed long
. Pero
-1L > 1UL
, debido a que -1L
es
promovido a unsigned long
y así parece ser un
gran número positivo.
Las convenciones también tienen lugar en las asignaciones; el valor del lado derecho es convertido al tipo de la izquierda, el cual es el tipo del resultado.
Un caráacter es convertido a un entero, tenga o no extensió de signo, como de describió anteriormente.
Los enteros más largos son convertidos a cortos o a
char
desechando el exceso de bits de más alto
orden. Así en
int i; char c; i = c; c = i;
el valor de c
no cambia. Esto es verdadero ya sea
que se in,iscuya o no la extensión de signo. Sin embargo, el
invertir el orden de las asignaciones podría producir
pérdida de información.
Si x
es float
e i
es
int
, entonces x = i
e i = x
producirán conversiones; de float
a
int
provoca el truncamiento de cualquier parte
fraccionaria. Cuando double
se convierte a
float
, el que se redondee o trunque el valor es
dependiente de la implantación.
Puesto que un argumento de la llamada a una función es
una expresión, también suceden conversiones de tipo
cuando se pasan argumentos a funciones. En ausencia del prototipo
de una función, char
y short
pasan
a ser int
, y float
se hace doble. Esta es
la razón por la que hemos declarado los argumentos a
funciones como int
y double
, aun cuando
la función se llama con char
y
float
.
Finalmente, la conversión explícita de tipo puede ser forzada ("coaccionada") en cualquier expresión, con un operador unario llamado cast. En la construcción
(nombre-de-tipo) expresión
la expresión es convertido al tipo nombrado, por
las reglas de conversión anterioires. El significado preciso
de un cast es como si la expresión fuera
asignada a una variable del tipo especificado, que se utiliza
entonces en lugar de la construcción completa. Por ejemplo,
la rutina de biblioteca sqrt
espera un argumento de
doble precisión y producirá resultados sin sentido si
maneja inadvertidamente algo diferente. (sqrt
está declarado en <math.h>
.) Así,
si n
es un entero, podemos usar
sqrt((double) n)
para convertir el valor de n
a doble antes de
pasarlo a sqrt
. Nótese que la conversión
forzosa produce el valor de n
en el tipo
apropiado; n
en sí no se altera. El operador
cast tiene la misma alta precedencia que otros operadores
unarios, como se resume en la tabla del final
de este capítulo.
Si un prototipo de función declara argumentos, como debe
ser normalmente, la declaración produce conversión
forzada automática de los argumentos cuando la
función es llamada. Así, dado el prototipo de la
función sqrt
:
double sqrt(double);
la llamada
raíz2 = sqrt(2);
obliga al entero 2 a ser el valor double
2.0 sin
necesidad de ningún cast.
La biblioteca estándar incluye una implantación transportable de un generador de números psuedoaleatorios, y una función para inicializar la semilla; lo primero ilustra un cast:
unsigned long int next = 1; /* rand: regresa un entero psuedoaleatorio en 0..32767 */ int rand(void) { next = next * 1103515245 + 12345; return (unsigned int)(next/65536) % 32768; } /* srand: fija la semilla para rand() */ void srand(unsigned int seed) { next = seed; }
Ejercicio 2-3. Escriba la función
htoi(s)
, que convierte una cadena de dígitos
hexadecimales (incluyendo 0x
ó 0X
en forma optativa) en su valor entero equivalente. Los
dígitos permitidos son del 0
al 9
,
de la a
a la f
, y de la A
a
la F
.
El lenguaje C proporciona do operadores poco comunes para
incrementar y decrementar variables. El operador de aumento
++
agrega 1 a su operando, en tanto que el operador de
disminución --
le resta 1. Hemos usado
frecuentamente ++
para incrementar variables, como
en
if (c == '\n') ++nl;
El aspecto poco común es que ++
y
--
pueden ser utilizado como prefijos (antes de la
variable, como en ++n
, o como postfijos
(después de la variable: n++
). En ambos casos,
el efecto es incrementar n
. Pero la expresión
++n
incrementa a n
antes de que su
valor se utilice, en tanto que n++
incrementa a
n
después de que su valor se ha
empleado. Esto significa que en un contexto donde el valor
está siendo utilizado, y no sólo el efecto,
++n
y n++
son diferentes. Si
n
es 5, entonces
x = n++;
asigna 5
a x
, pero
x = ++n;
hace que x
sea 6. En ambos caso, n
se
hace 6. Los operadores de incremento y decremento sólo
pueden aplicarse a variables; una expresión como
(i+j)++
es ilegal.
Dentro de un contexto en donde no se desea ningún valor, sino sólo el efecto de incremento, como en
if (c == '\n') nl++;
prefijo y postfijo son iguales. Pero existen situaciones en
donde se require especificamente uno u otro. Por ejemplo,
considérese la función squeeze(s,c)
, que
elimina todas las ocurrencias del carácter c
de
una cadena s
.
/* squeeze: borra todas las c de s */ void squeeze(char s[], int c) { int i, j; for (i = j = 0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[j] = '\0'; }
Cada vez que se encuentra un valor diferente de c
,
éste se copia en la posición actual j
, y
sólo entonces j
es incrementada para prepararla
para el siguiente carácter. Esto es exactamente equivalente
a
if (s[i] != c) { s[j] = s[i]; j++; }
Otro ejemplo de construcciónsemejante viene de la
función getline
que escribimos en el capítulo 1, en donde podemos reemplazar
if (c == '\n') { s[i] = c; ++i; }
por algo más compacto como
if (c == '\n') s[i++] = c;
Como un tercer ejemplo, considéres que la función
estándar strcat(s,t)
, que concatena la cadena
t
al final de la cadena s
.
strcat
supone que hay suficiente espacio en
s
para almacenar la combinación. Como la hemos
escrito, strcat
no regresa un valor; la versión
de la biblioteca estándar regresa un apuntador a la cadena
resultante.
/* strcat: concatena t al final de s; s debe ser suficientamente grande */ void strcat(char s[], char t[]) { int i, j; i = j = 0; while (s[i] != '\0') /* encuentra el fin de s */ i++; while (s[i++] = t[j++]) != '\0') /* copia t */ ; }
Como cada carácter se copia de t
a
s
, el ++
postfijo se aplica tanto a
i
como a j
para estar seguros de que
ambos están en posición para la siguiente
iteración.
Ejercicio 2-4. Escriba una versión
alterna de squeeze(s1,s2)
que borre cada
carácter de sl
que coincida con cualquier
carácter de la cadena s2.
.
Ejercicio 2-5. Escriba la función
any(s1,s2)
, que regresa la primera posición de
la cadena sl
en donde se encuentre qualquier
carácter de la cadena s2
, o -1 si
sl
no contiene caracteres de s2
. (La
función de biblioteca estándar strpbrk
hace el mismo trabajo pero regresa un apuntador a la
posición encontrada.)
El lenguaje C proporciona seis operadores para manejo de bits;
sólo pueden ser aplicados a operadores integrales, esto es,
char, short, int
y long
, con o sin
signo.
& AND de bits | OR inclusivo de bits ^ OR exclusivo de bits << corrimiento a la izquierda >> corrimiento a la derecha ~ complemento a uno (unario)
El operador AND de bits &
a menudo es usado
para enmascarar algún conjunto de bits; por ejemplo,
n = n & 0177;
hace cero todos los bits de n
, menos los 7 de orden
menor.
El operador OR
de bits |
es empleado
para encender bits:
x = x | SET_ON;
fija en uno a todos los bits de x
que son uno en
SET_ON
.
El operador OR exclusivo ^
pone en uno en cada
posición en donde sus operandos tienen bits diferentes, y
cero en donde son iguales.
Se deben distinguir los operadores de bits &
y
|
de los operadores lógicos
&&
y ||
, que implican
evaluación de izquierda a derecha de un valor de verdad. Por
ejemplo, si x
es 1 y y
es 2, entonces
x & y
es cero en tanto que x &&
y
es uno.
Los operadores de corrimiento <<
y
>>
realizan corrimientos a la izquierda, el
número de posiciones de bits dado por el operadando de la
derecha, el cual debe ser positivo. Así x <<
2
desplaza el valor de x
a la izquierda dos
posiciones, llenando los bits vacantes con cero; esto es
equivalente a una multiplicación por 4. El correr a la
derecha una cantidad unsigned siempre llena los bits
vacantes con cero. El correr a la derecha una cantidad signada
llenará con bits de signo ("corrimiento aritmético")
en algunas máquinas y con bits 0 ("corrimiento
lógico") en otras.
El operador unario ~
da el complemento de un
entero; esto es, convierte cada bit 1 en un bit 0 y viceversa. Por
ejemplo,
x = x & ~077
fija los últimos seis bits de x
en cero.
Nótese que x & ~077
es independiente de la
longitud de la palabra, y por lo tanto, es preferible a, por
ejemplo, x & 0177700
, que supone que x es una
cantidad de 16 bits. La forma transportable no involucra un costo
extra, puesto que ~077
es una expresión
constante que puede ser evaluado en tiemp de
compilación.
Como ilustración de algunos de los operadores de bits,
considere la función getbits(x,p,n)
que regresa
el campo de n
bits de x
(ajustado a la
derecha) que principia en la posición p
Se
supone que la posición del bit o está en el borde
derecho y que n
y p
son valores poitivos
adecuados. Por ejemplo, getbits(x,4,3)
regresa los
tres bits que están en la posición 4, 3, y 2,
ajustados a la derecha.
/* getbits: obtiene n bits desde la posición p */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p+1-n)) & ~(~0 << n); }
La expresión x >> (p+1-nd) mueve el campo
deseado al borde derecho de la palabra.
~0 es todos los bits
en 1; corriendo n
bits hacia la izquierda con
~0<<n
coloca ceros en los n
bits
más a la derecha; complementando con ~
hace una
máscara de unos en los n
bits más a la
derecha.